<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/normalize/7.0.0/normalize.min.css" rel="stylesheet">
</head>
<body>
<h2>发布-订阅模式</h2>
<h3>1. 定义</h3>
<p>也称作观察者模式，定义了对象间的一种一对多的依赖关系，当一个对象的状态发 生改变时，所有依赖于它的对象都将得到通知</p>

<h3>2. 核心</h3>
<p>取代对象之间硬编码的通知机制，一个对象不用再显式地调用另外一个对象的某个接口。</p>

<h3>3. 优缺点</h3>
<p>优点：一为时间上的解耦，二为对象之间的解耦。可以用在异步编程中与MV*框架中</p>
<p>缺点：创建订阅者本身要消耗一定的时间和内存，订阅的处理函数不一定会被执行，驻留内存有性能开销<br>
弱化了对象之间的联系，复杂的情况下可能会导致程序难以跟踪维护和理解</p>

<h3>4. 场景</h3>
<p>JS中的事件就是经典的发布-订阅模式的实现</p>

<div>
    <button id="count">点我</button>
    <div id="show"></div>
</div>
<script type="text/javascript">
/*// 订阅
document.body.addEventListener('click', function() {
    console.log('click1');
}, false);

// 发布
document.body.click();*/


//观察者
var observer = {
    //订阅集合
    subscribes: [],
    //订阅
    subscribe(type, fn) {
        if (!this.subscribes[type]) {
            this.subscribes[type] = []
        }
        //收集订阅者的处理
        typeof fn === 'function' && this.subscribes[type].push(fn)
    },
    //发布，可能会携带一些信息发布出去
    publish() {
        var args = [].slice.call(arguments),
            type = args.shift(),
            fns = this.subscribes[type]

        //不存在的订阅类型，以及订阅时未传入回调
        if (!fns || !fns.length) return

        //挨个处理调用
        for (var i = 0; i < fns.length; i++) {
            fns[i].apply(this, args);
        }
    },
    //删除订阅
    remove(type, fn) {
        //删除全部
        if (typeof type === 'undefined') {
            this.subscribes = [];
            return;
        }

        var fns = this.subscribes[type];

        //不存在订阅的类型，以及订阅时未传入处理回调
        if (!fns || !fns.length) return

        if (typeof fn == 'undefined') {
            fns.length = 0;
            return;
        }

        //挨个处理
        for (var i = 0; i < fns.length; i++) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
}


// A订阅岗位
observer.subscribe('job', function (jobs) {
     console.log('A', jobs);
});
// B订阅岗位
observer.subscribe('job' , function (jobs) {
     console.log('B', jobs);
});


/*// A订阅了笔试成绩
observer.subscribe('examinationA', function(score) {
    console.log(score);
});

// B订阅了笔试成绩
observer.subscribe('examinationB', function(score) {
    console.log(score);
});

// A订阅了面试结果
observer.subscribe('interviewA', function(result) {
    console.log(result);
});*/


/*observer.publish('examinationA', 100); // 100
observer.publish('examinationB', 80); // 80
observer.publish('interviewA', '备用'); // 备用

*/
observer.publish('job', ['前端', '后端', '测试']); // 输出A和B的岗位

// A都取消订阅了岗位
observer.remove('job')


//============================================================
var event = {
    list: [],
    listen(key, fn) {
        if (!this.list[key]) {
            //如果还没有订阅此类消息，给该类消息创建一个缓存列表
            this.list[key] = []
        }
        this.list[key].push(fn)
    },
    trigger() {     //shoeObj.trigger("red",40);
        var args = [].slice.call(arguments),
            key = args.shift(),     //取出消息类型名称
            fns = this.list[key];   //取出该消息对应的回调函数的集合

        //如果没有订阅该消息，则返回
        if (!fns || fns.length === 0) {
            return;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, args);   // args 是发布消息时附送的参数
        }
    },
    //删除订阅
    remove(key, fn) {
        //删除全部
        if (typeof key === 'undefined') {
            this.list = [];
            return;
        }

        var fns = this.list[key];

        //不存在订阅的类型，以及订阅时未传入处理回调
        if (!fns || !fns.length) return

        if (typeof fn == 'undefined') {
            fns.length = 0;
            return;
        }

        //挨个处理
        for (var i = 0; i < fns.length; i++) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
}

var initEvent = function(obj) {
    for(var i in event) {
        obj[i] = event[i];
    }
};
// var shoeObj = Object.assign({}, event);

// 我们再来测试下，我们还是给shoeObj这个对象添加发布-订阅功能；
var shoeObj = {};
initEvent(shoeObj);

// 小红订阅如下消息
shoeObj.listen('red',fn1 = function(size){
    console.log("尺码是："+size);  
});

// 小花订阅如下消息
shoeObj.listen('red',fn2 = function(size){
    console.log("再次打印尺码是："+size); 
});

console.log(fn1)

// shoeObj.remove("red",fn1);
shoeObj.trigger("red",42);


//============================================
var Event = (function () {
    var list = {},
        listen,
        trigger,
        remove;

    listen = function (key, fn) {
        // console.log(key)
        if (!list[key]) {
            //如果还没有订阅此类消息，给该类消息创建一个缓存列表
            list[key] = []
        }
        list[key].push(fn)
    };
    trigger = function () {     //shoeObj.trigger("red",40);
        var args = [].slice.call(arguments),
            key = args.shift(),     //取出消息类型名称
            fns = list[key];   //取出该消息对应的回调函数的集合

        //如果没有订阅该消息，则返回
        if (!fns || fns.length === 0) {
            return;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, args);   // args 是发布消息时附送的参数
        }
    }
    remove = function (key, fn) {
        var fns = list[ key ];
        if ( !fns ){
            return false;
        }
        if ( !fn ){
            fns && ( fns.length = 0 );
        } else {
            //挨个处理
            for (var i = 0; i < fns.length; i++) {
                if (fns[i] === fn) {
                    fns.splice(i, 1);
                }
            }
        }
    };
    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})()

/*console.log(Event)
// 测试代码如下：

Event.listen("color",function(size) {
    console.log("尺码为:"+size); // 打印出尺码为42
});
Event.trigger("color",42);*/

var a = (function(){
    var count = 0;
    var button = document.getElementById( 'count' );
    button.onclick = function(){
        Event.trigger( 'add', count++ );
    }
})();
var b = (function(){
    var div = document.getElementById( 'show' );
    Event.listen( 'add', function( count ){
        div.innerHTML = count;
    });
})();



/*************** 具体代码 **********************/ 
var Event = (function(){
    var global = this,
        Event,
        _default = 'default';
    Event = function(){
        var _listen,
            _trigger,
            _remove,
            _slice = Array.prototype.slice,
            _shift = Array.prototype.shift,
            _unshift = Array.prototype.unshift,
            namespaceCache = {},
            _create,
            find,
            each = function( ary, fn ){
                var ret;
                for ( var i = 0, l = ary.length; i < l; i++ ){
                    var n = ary[i];
                    ret = fn.call( n, i, n);
                }
                return ret;
            };
        _listen = function( key, fn, cache ){
                if ( !cache[ key ] ){
                    cache[ key ] = [];
                }
                cache[key].push( fn );
        };
        _remove = function( key, cache ,fn){
                if ( cache[ key ] ){
                    if( fn ){
                        for( var i = cache[ key ].length; i >= 0; i-- ){
                            if( cache[ key ] === fn ){
                                cache[ key ].splice( i, 1 );
                            }
                        }
                    }else{
                        cache[ key ] = [];
                    }
                }
        };
        _trigger = function(){
                var cache = _shift.call(arguments),
                    key = _shift.call(arguments),
                    args = arguments,
                    _self = this,
                    ret,
                    stack = cache[ key ];
                if ( !stack || !stack.length ){
                    return;
                }
                return each( stack, function(){
                    return this.apply( _self, args );
                });
        };
        _create = function( namespace ){
                var namespace = namespace || _default;
                var cache = {},
                    offlineStack = [], // 离线事件
                    ret = {
                        listen: function( key, fn, last ){
                            _listen( key, fn, cache );
                            if ( offlineStack === null ){
                                return;
                            }
                            if ( last === 'last' ){
                                offlineStack.length && offlineStack.pop()()
                            }else{
                                each( offlineStack, function(){
                                    this();
                                });
                            }
                            offlineStack = null;
                        },
                        one: function( key, fn, last ){
                            _remove( key, cache );
                            this.listen( key, fn ,last );
                        },
                        remove: function( key, fn ){
                            _remove( key, cache ,fn);
                        },
                        trigger: function(){
                            var fn,
                                args,
                                _self = this;
                            _unshift.call( arguments, cache );
                            args = arguments;
                            fn = function(){
                                return _trigger.apply( _self, args );
                            };
                            if ( offlineStack ){
                                return offlineStack.push( fn );
                            }
                            return fn();
                        }
                    };
                return namespace ?
                ( namespaceCache[ namespace ] ? namespaceCache[ namespace ] :
                    namespaceCache[ namespace ] = ret )
                : ret;
        };
        return {
            create: _create,
            one: function( key,fn, last ){
                var event = this.create( );
                event.one( key,fn,last );
            },
            remove: function( key,fn ){
                var event = this.create( );
                event.remove( key,fn );
            },
            listen: function( key, fn, last ){
                var event = this.create( );
                event.listen( key, fn, last );
            },
            trigger: function(){
                var event = this.create( );
                event.trigger.apply( this, arguments );
            }
        };
    }();
    return Event;
})();

</script>
</body>
</html>